/**
 * Copyright (C) 2006 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.guicerecipes.support.internal;

import java.io.*;
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

import org.guicerecipes.util.*;

import com.google.common.collect.*;
import com.google.inject.*;
import com.google.inject.spi.*;

import static com.google.common.base.Preconditions.*;

/**
 * A collection of error messages. If this type is passed as a method parameter, the method is considered to have executed succesfully only if new errors were not added to this collection.
 * 
 * @author jessewilson@google.com (Jesse Wilson)
 */
public final class Errors implements Serializable {

	private static final long serialVersionUID = 3899546906740856009L;

	// TODO(kevinb): gee, ya think we might want to remove this?
	private static final boolean allowNullsBadBadBad = "I'm a bad hack".equals(System.getProperty("guice.allow.nulls.bad.bad.bad"));

	private List<Message> errors;
	private final List<Object> sources;

	public Errors() {
		sources = Lists.newArrayList();
		errors = Lists.newArrayList();
	}

	public Errors(Object source) {
		sources = Lists.newArrayList(source);
		errors = Lists.newArrayList();
	}

	private Errors(Errors parent, Object source) {
		errors = parent.errors;
		sources = Lists.newArrayList(parent.sources);
		sources.add(source);
	}

	/**
	 * Returns an instance that uses {@code source} as a reference point for newly added errors.
	 */
	public Errors withSource(Object source) {
		return source == SourceProvider.UNKNOWN_SOURCE ? this : new Errors(this, source);
	}

	public void pushSource(Object source) {
		sources.add(source);
	}

	public void popSource(Object source) {
		Object popped = sources.remove(sources.size() - 1);
		checkArgument(source == popped);
	}

	/**
	 * We use a fairly generic error message here. The motivation is to share the same message for both bind time errors:
	 * 
	 * <pre>
	 * <code>Guice.createInjector(new AbstractModule() {
	 *   public void configure() {
	 *     bind(Runnable.class);
	 *   }
	 * }</code>
	 * </pre>
	 * 
	 * ...and at provide-time errors:
	 * 
	 * <pre>
	 * <code>Guice.createInjector().getInstance(Runnable.class);</code>
	 * </pre>
	 * 
	 * Otherwise we need to know who's calling when resolving a just-in-time binding, which makes things unnecessarily complex.
	 */
	public Errors missingImplementation(Object keyOrType) {
		return addMessage("No implementation for %s was bound.", keyOrType);
	}

	public Errors converterReturnedNull(String stringValue, Object source, TypeLiteral<?> type, MatcherAndConverter matchingConverter) {
		return addMessage("Received null converting '%s' (bound at %s) to %s%n" + " using %s.", stringValue, source, type, matchingConverter);
	}

	public Errors conversionTypeError(String stringValue, Object source, TypeLiteral<?> type, MatcherAndConverter matchingConverter, Object converted) {
		return addMessage("Type mismatch converting '%s' (bound at %s) to %s%n" + " using %s.%n" + " Converter returned %s.", stringValue, source, type, matchingConverter, converted);
	}

	public Errors conversionError(String stringValue, Object source, TypeLiteral<?> type, MatcherAndConverter matchingConverter, Exception cause) {
		return addMessage(cause, "Error converting '%s' (bound at %s) to %s%n" + " using %s.%n" + " Reason: %s", stringValue, source, type, matchingConverter, cause);
	}

	public Errors ambiguousTypeConversion(String stringValue, Object source, TypeLiteral<?> type, MatcherAndConverter a, MatcherAndConverter b) {
		return addMessage("Multiple converters can convert '%s' (bound at %s) to %s:%n" + " %s and%n" + " %s.%n" + " Please adjust your type converter configuration to avoid overlapping matches.",
			stringValue, source, type, a, b);
	}

	public Errors bindingToProvider() {
		return addMessage("Binding to Provider is not allowed.");
	}

	public Errors subtypeNotProvided(Class<? extends Provider<?>> providerType, Class<?> type) {
		return addMessage("%s doesn't provide instances of %s.", providerType, type);
	}

	public Errors notASubtype(Class<?> implementationType, Class<?> type) {
		return addMessage("%s doesn't extend %s.", implementationType, type);
	}

	public Errors recursiveImplementationType() {
		return addMessage("@ImplementedBy points to the same class it annotates.");
	}

	public Errors recursiveProviderType() {
		return addMessage("@ProvidedBy points to the same class it annotates.");
	}

	public Errors missingRuntimeRetention(Object source) {
		return addMessage("Please annotate with @Retention(RUNTIME).%n" + " Bound at %s.", source);
	}

	public Errors missingScopeAnnotation() {
		return addMessage("Please annotate with @ScopeAnnotation.");
	}

	public Errors optionalConstructor(Constructor<?> constructor) {
		return addMessage("%s is annotated @Inject(optional=true), " + "but constructors cannot be optional.", constructor);
	}

	public Errors cannotBindToGuiceType(String simpleName) {
		return addMessage("Binding to core guice framework type is not allowed: %s.", simpleName);
	}

	public Errors scopeNotFound(Class<? extends Annotation> scopeAnnotation) {
		return addMessage("No scope is bound to %s.", scopeAnnotation);
	}

	public Errors scopeAnnotationOnAbstractType(Class<? extends Annotation> scopeAnnotation, Class<?> type, Object source) {
		return addMessage("%s is annotated with %s, but scope annotations are not supported " + "for abstract types.%n Bound at %s.", type, scopeAnnotation, source);
	}

	public Errors misplacedBindingAnnotation(Member member, Annotation bindingAnnotation) {
		return addMessage("%s is annotated with %s, but binding annotations should be applied " + "to its parameters instead.", member, bindingAnnotation);
	}

	private static final String CONSTRUCTOR_RULES = "Classes must have either one (and only one) constructor " + "annotated with @Inject or a zero-argument constructor that is not private.";

	public Errors missingConstructor(Class<?> implementation) {
		return addMessage("Could not find a suitable constructor in %s. " + CONSTRUCTOR_RULES, implementation);
	}

	public Errors tooManyConstructors(Class<?> implementation) {
		return addMessage("%s has more than one constructor annotated with @Inject. " + CONSTRUCTOR_RULES, implementation);
	}

	public Errors duplicateScopes(Scope existing, Class<? extends Annotation> annotationType, Scope scope) {
		return addMessage("Scope %s is already bound to %s. Cannot bind %s.", existing, annotationType, scope);
	}

	public Errors missingConstantValues() {
		return addMessage("Missing constant value. Please call to(...).");
	}

	public Errors cannotInjectInnerClass(Class<?> type) {
		return addMessage("Injecting into inner classes is not supported.  " + "Please use a 'static' class (top-level or nested) instead of %s.", type);
	}

	public Errors duplicateBindingAnnotations(Member member, Class<? extends Annotation> a, Class<? extends Annotation> b) {
		return addMessage("%s has more than one annotation annotated with @BindingAnnotation: " + "%s and %s", member, a, b);
	}

	public Errors duplicateScopeAnnotations(Class<? extends Annotation> a, Class<? extends Annotation> b) {
		return addMessage("More than one scope annotation was found: %s and %s", a, b);
	}

	public Errors recursiveBinding() {
		return addMessage("Binding points to itself.");
	}

	public Errors bindingAlreadySet(Key<?> key, Object source) {
		return addMessage("A binding to %s was already configured at %s.", key, source);
	}

	public Errors childBindingAlreadySet(Key<?> key) {
		return addMessage("A binding to %s already exists on a child injector.", key);
	}

	public Errors errorInjectingMethod(Throwable cause) {
		return addMessage(cause, "Error injecting method, %s", cause);
	}

	public Errors errorInjectingConstructor(Throwable cause) {
		return addMessage(cause, "Error injecting constructor, %s", cause);
	}

	public Errors errorInProvider(RuntimeException runtimeException, Errors errorsFromException) {
		if (errorsFromException != null) {
			return merge(errorsFromException);
		} else {
			return addMessage(runtimeException, "Error in custom provider, %s", runtimeException);
		}
	}

	public Errors cannotInjectRawProvider() {
		return addMessage("Cannot inject a Provider that has no type parameter");
	}

	public Errors cannotSatisfyCircularDependency(Class<?> expectedType) {
		return addMessage("Tried proxying %s to support a circular dependency, but it is not an interface.", expectedType);
	}

	public Errors makeImmutable() {
		errors = ImmutableList.copyOf(errors);
		return this;
	}

	public void throwCreationExceptionIfErrorsExist() {
		if (!hasErrors()) {
			return;
		}

		makeImmutable();
		throw new CreationException(getMessages());
	}

	private Message merge(Message message) {
		List<Object> sources = Lists.newArrayList();
		sources.addAll(this.sources);
		sources.addAll(message.getSources());
		return new Message(stripDuplicates(sources), message.getMessage(), message.getCause());
	}

	public Errors merge(Collection<Message> messages) {
		if (messages != errors) {
			for (Message message : messages) {
				errors.add(merge(message));
			}
		}
		return this;
	}

	public Errors merge(Errors moreErrors) {
		merge(moreErrors.errors);
		return this;
	}

	public void throwIfNecessary() throws ErrorsException {
		if (!hasErrors()) {
			return;
		}

		throw toException();
	}

	public ErrorsException toException() {
		return new ErrorsException(this);
	}

	public boolean hasErrors() {
		return !errors.isEmpty();
	}

	private Errors addMessage(String messageFormat, Object... arguments) {
		return addMessage(null, messageFormat, arguments);
	}

	public Errors addMessage(Throwable cause, String messageFormat, Object... arguments) {
		String message = format(messageFormat, arguments);
		addMessage(new Message(stripDuplicates(sources), message, cause));
		return this;
	}

	public Errors addMessage(Message message) {
		errors.add(message);
		return this;
	}

	private String format(String messageFormat, Object... arguments) {
		for (int i = 0; i < arguments.length; i++) {
			arguments[i] = Errors.convert(arguments[i]);
		}
		return String.format(messageFormat, arguments);
	}

	public List<Message> getMessages() {
		List<Message> result = Lists.newArrayList(errors);

		Collections.sort(result, new Comparator<Message>() {
			public int compare(Message a, Message b) {
				return a.getSource().compareTo(b.getSource());
			}
		});

		return result;
	}

	public static String format(String heading, Collection<? extends Message> errorMessages) {
		Formatter fmt = new Formatter().format(heading).format(":%n%n");
		int index = 1;
		for (Message errorMessage : errorMessages) {
			fmt.format("%s) %s%n", index++, errorMessage.getMessage());

			List<Object> dependencies = errorMessage.getSources();
			for (int i = dependencies.size() - 1; i >= 0; i--) {
				Object source = dependencies.get(i);

				if (source instanceof Dependency<?>) {
					Dependency<?> dependency = (Dependency<?>) source;

					InjectionPoint injectionPoint = dependency.getInjectionPoint();
					if (injectionPoint != null) {
						Member member = injectionPoint.getMember();
						if (member instanceof Field) {
							fmt.format("  for field at %s%n", StackTraceElements.forMember(member));
						} else if ((member instanceof Method) || (member instanceof Constructor<?>)) {
							fmt.format("  for parameter %s at %s%n", dependency.getParameterIndex(), StackTraceElements.forMember(member));
						} else {
							throw new AssertionError();
						}
					}
					continue;
				}

				fmt.format("  at %s%n", sourceToString(source));
			}

			fmt.format("%n");
		}

		return fmt.format("%s error[s]", errorMessages.size()).toString();
	}

	/**
	 * Returns {@code value} if it is non-null allowed to be null. Otherwise a message is added and an {@code ErrorsException} is thrown.
	 */
	public <T> T checkForNull(T value, Object source, Dependency<?> dependency) throws ErrorsException {
		if ((value != null) || dependency.isNullable() || allowNullsBadBadBad) {
			return value;
		}

		int parameterIndex = dependency.getParameterIndex();
		String parameterName = (parameterIndex != -1) ? "parameter " + parameterIndex + " of " : "";
		addMessage("null returned by binding at %s%n but %s%s is not @Nullable", source, parameterName, dependency.getInjectionPoint().getMember());

		throw toException();
	}

	private static abstract class Converter<T> {

		final Class<T> type;

		Converter(Class<T> type) {
			this.type = type;
		}

		boolean appliesTo(Object o) {
			return type.isAssignableFrom(o.getClass());
		}

		String convert(Object o) {
			return toString(type.cast(o));
		}

		abstract String toString(T t);
	}

	@SuppressWarnings("unchecked")
	private static final Collection<Converter<?>> converters = ImmutableList.of(new Converter<MatcherAndConverter>(MatcherAndConverter.class) {
		@Override
		public String toString(MatcherAndConverter m) {
			return m.toString();
		}
	}, new Converter<Class>(Class.class) {
		@Override
		public String toString(Class c) {
			return c.getName();
		}
	}, new Converter<Member>(Member.class) {
		@Override
		public String toString(Member member) {
			return member.toString();
		}
	}, new Converter<Key>(Key.class) {
		@Override
		public String toString(Key k) {
			StringBuilder result = new StringBuilder();
			result.append(k.getTypeLiteral());
			if (k.getAnnotationType() != null) {
				result.append(" annotated with ");
				result.append(k.getAnnotation() != null ? k.getAnnotation() : k.getAnnotationType());
			}
			return result.toString();
		}
	});

	public static Object convert(Object o) {
		for (Converter<?> converter : converters) {
			if (converter.appliesTo(o)) {
				return converter.convert(o);
			}
		}
		return o;
	}

	/**
	 * This method returns a String that indicates an element source. We do a best effort to include a line number in this String.
	 */
	public static String sourceToString(Object source) {
		checkNotNull(source, "source");

		if (source instanceof InjectionPoint) {
			return sourceToString(((InjectionPoint) source).getMember());
		} else if (source instanceof Member) {
			return StackTraceElements.forMember((Member) source).toString();
		} else if (source instanceof Class<?>) {
			return StackTraceElements.forType(((Class<?>) source)).toString();
		} else {
			return convert(source).toString();
		}
	}

	/**
	 * Removes consecutive duplicates, so that [A B B C D A] becomes [A B C D A].
	 */
	private <T> List<T> stripDuplicates(List<T> list) {
		list = Lists.newArrayList(list);

		Iterator<?> i = list.iterator();
		if (i.hasNext()) {
			for (Object last = i.next(), current; i.hasNext(); last = current) {
				current = i.next();
				if (last.equals(current)) {
					i.remove();
				}
			}
		}

		return ImmutableList.copyOf(list);
	}
}
